/**************************************************************************************
* Mythos.java
* Runs the game
* 
* Primary game "container" is the GameMap.
GameMap contains the layout(Cells), Terrain objects (used by all cells in the map), Actor objects, Field(reactive but not active) objects.
When a new Game is started, the all GameMaps are created as "shell" maps - they are given basic terrain info, exit data and assigned any fixed, custom content, but have no layout, monsters, or items.

The Vault stores static base information for generating Actors, Items, etc. The vault performs random generation and is static (information taken from data files)

GameWorld is the top container that is serialized/unserialized for save/load actions. It is the state model that encapsulates everything.
GameWorld contains:
	-All GameMap objects 
	-All global "world" data - Quest states, the player object, etc
	-Exit data. Exits are paired up by a unqiue (for the pair) LinkID number so that when something takes an exit, the GameWorld can take the exit's GameMap and LinkID and determine both the GameMap to load as well as the cell to place them on.
	
	When changing maps, adjacent actors may come with the player, and nearby followers are also coming through. Actors remaining on the level receive full healing/curing and either return to their fixed location or are randomly placed.
	Actors switching maps join an entrance queue. The player is placed immediately, and as many creatures from the entrance queue will be placed as will fit in the adjacent cells to the player. Remaining creatures will be placed as these spaces become available.
	
The game architecture is as follows:
	-Mythos: Starts and updates the GUI, decides what to do with player input
	-GameWorld: the current game state
	-GameWindow: the GUI. Has drawFoo() functions that are called by the main thread to update the screen with the current map, player data, target data, message log, etc. The output side of the GUI is passive, not changing until told to do so. The input side is active, passing player commands to the handler function.
	-RNG, the RNG.
	
	Handler Functions: These methods of the main class determine what to do with the response from the user. There is a state variable that determines which handler is currently in use, and a parent handler which is always called by the GUI that uses this state variable to determine which handler reacts. Handlers include HandleAction (used when it is the Player's turn to act), HandleMenu (when there is a menu or text display open), HandleCursor (for looking, targeting)
	
DISPLAYING TEXT
-Panel to draw on (fullPanel to do the whole window, playPanel to just do the play area window, still showing player stats, logs, etc.
-Title
-Array of paragraphs (ColoredString[])
-first line displayed (int)
-last possible first line (int: array size - number of displayed lines on screen, min 1)

-When player goes up/down move screen up down by one, with pageup/space & pagedown shift first line and redraw the screen

DISPLAYING MENU
-Panel to draw on (fullPanel to do the whole window, playPanel to just do the play area window, still showing player stats, logs, etc.
-Title
-Instructions/general remarks at top of screen
-Array of choices to be shown (MenuItem[])
-whether to show a details panel (boolean)
-current choice (if showing details).

Details is basically a menu itself - it has title, remarks, array of MenuChoices
-on cancel, on choose commands (ints, used to navigate back/forwards, if there is a submenu that is forward command, if a target is needed 

GENERAL PLAY
-Use keys to move/melee/switch. Shift+keys does forced action instead of moving (force attack1, close doors, etc)
-Player has ability slots bound to the number keys,-,=,`. These are shown, along with current pools, attack1/defense, etc.
-Speed has five settings, and mostly only applies to moving. Everything else takes one turn.
-Towns/bases are used to train skills / guild promotions.
-Player finds items and 
*************************************************************************************/

package creid.mythos.engine;

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Random;

import javax.swing.JFileChooser;

import creid.mythos.ui.GameUI;

public class Mythos implements KeyListener
{
	//Constants/////////////////////////////////////////////////////////////////////////

	//Title
	public static final String VERSION = "0.5.7.6";
	public static final String TITLE = "Mythos " + VERSION;
	
	//Dev or release
	public static final boolean RELEASE = false;
	
	//Debug settings
	public static final boolean DEBUG_MAP = false;
	public static final boolean DEBUG_NO_MOB_GEN = false;
	public static final boolean DEBUG_AUTO_CHARGEN = false;
	
	//Help context
	public static final int HELP_PLAYER_TURN = 0;
	
	//Commands
	public static final int NEW_GAME = KeyEvent.VK_N;
	public static final int LOAD_GAME = KeyEvent.VK_R;
	public static final int EXIT_QUIT = KeyEvent.VK_Q;
	public static final int EXIT_ESCAPE = KeyEvent.VK_ESCAPE;
	
	public static final int ANY_KEY = 0;
	public static final int MAIN_MENU = 1;
	public static final int PLAYER_ACTION = 2;
	public static final int CONFIRM = 3;
	public static final int TEXT_ENTRY = 4;
	public static final int MENU = 5;
	public static final int DIRECTION = 6;
	
	//Move commands
	public static final int MOVE_N = KeyEvent.VK_K;
	public static final int MOVE_N2 = KeyEvent.VK_UP;
	public static final int MOVE_N3 = KeyEvent.VK_NUMPAD8;
	public static final int MOVE_S = KeyEvent.VK_J;
	public static final int MOVE_S2 = KeyEvent.VK_DOWN;
	public static final int MOVE_S3 = KeyEvent.VK_NUMPAD2;
	public static final int MOVE_E = KeyEvent.VK_L;
	public static final int MOVE_E2 = KeyEvent.VK_RIGHT;
	public static final int MOVE_E3 = KeyEvent.VK_NUMPAD6;
	public static final int MOVE_W = KeyEvent.VK_H;
	public static final int MOVE_W2 = KeyEvent.VK_LEFT;
	public static final int MOVE_W3 = KeyEvent.VK_NUMPAD4;
	public static final int MOVE_NW = KeyEvent.VK_Y;
	public static final int MOVE_NW2 = KeyEvent.VK_NUMPAD7;
	public static final int MOVE_NE = KeyEvent.VK_U;
	public static final int MOVE_NE2 = KeyEvent.VK_NUMPAD9;
	public static final int MOVE_SW = KeyEvent.VK_B;
	public static final int MOVE_SW2 = KeyEvent.VK_NUMPAD1;
	public static final int MOVE_SE = KeyEvent.VK_N;
	public static final int MOVE_SE2 = KeyEvent.VK_NUMPAD3;
	public static final int WAIT = KeyEvent.VK_NUMPAD5;
	public static final int WAIT2 = KeyEvent.VK_PERIOD;
	public static final int OPERATE = KeyEvent.VK_O;
	public static final int GROUND_ACT = KeyEvent.VK_G;
	public static final int SWAP_WEAPONS = KeyEvent.VK_S;
	public static final int THROW = KeyEvent.VK_T;
	public static final int FIRE = KeyEvent.VK_F;
	public static final int RELOAD = KeyEvent.VK_R;

	//Misc Action commands
	public static final int EQUIPMENT = KeyEvent.VK_E;
	public static final int INVENTORY = KeyEvent.VK_I; 
	public static final int NEXT_TARGET = KeyEvent.VK_TAB; 

	//Meta/UI commands
	public static final int LOOK = KeyEvent.VK_X;
	public static final int SET_TARGET = KeyEvent.VK_T;
	public static final int SHOW_HELP = KeyEvent.VK_F1;
	public static final int SHOW_HELP2 = KeyEvent.VK_SLASH; //? when shifted
	public static final int SHOW_OLD_MESSAGES = KeyEvent.VK_P; //? when shifted
	
	public static final int CONFIRM_YES = KeyEvent.VK_Y;
	public static final int CONFIRM_NO = KeyEvent.VK_N;
	public static final int CONFIRM_DEFAULT = KeyEvent.VK_ENTER;
	public static final int CONFIRM_DEFAULT2 = KeyEvent.VK_SPACE;
	public static final int CANCEL = KeyEvent.VK_ESCAPE;
	
	public static final int BACKSPACE = KeyEvent.VK_BACK_SPACE;
	public static final int BACKSPACE2 = KeyEvent.VK_DELETE;
	
	public static final int SAVE_AND_QUIT = KeyEvent.VK_S;
	
	public static final int QUIT_SUICIDE = KeyEvent.VK_Q;

	public static final int ANIMATION_DELAY = 150;	//Delay in milliseconds between animation steps
	
	//Menu constants
	public static final int MENU_CANCELLED = -1;
	public static final int MENU_DETAIL_MULT = 1000;
	public static final boolean NUMBERED_MENU = true;
	public static final boolean LETTERED_MENU = false;
	public static final String MENU_HEADER_SEPARATOR = "============================================================================";
	public static final String MENU_INSTRUCTIONS = "Press the letter in parentheses to select, or Escape to cancel";
	public static final String MENU_INSTRUCTIONS_NUM = "Press the number in parentheses to select, or Escape to cancel";
	public static final String MENU_INSTRUCTIONS_CONFIRM = "Press Enter to confirm, any other key to cancel";
	public static final int[] SIMPLE_CONFIRM = { CONFIRM_YES, CONFIRM_NO };
	
	public static final String BIRTH_MENU_HEADER = "Choose a background. Your background determines starting attributes and equipment";
	public static final String INVENTORY_HEADER = "Select an item to view, use, or drop it.";
	public static final String EQUIPMENT_HEADER = "Select a slot to wear something or to take off the currently worn item";
	public static final String PICK_EQUIP_HEADER = "Select an item to wield or wear";
	public static final String THROW_HEADER = "Select an item to throw";
	
	public static final int INV_DROP = KeyEvent.VK_D;
	public static final int INV_USE = KeyEvent.VK_U;
	
	public static final String LOOK_INSTRUCTIONS = "Use arrow/WASD/numpad keys to move cursor, 't' to target, ESC to quit";
	
	//Attributes////////////////////////////////////////////////////////////////////////
	
	private static Random randomizer;
	public static MessageBuffer logger;
	
	//Game World
	private GameWorld game;
	
	//static Entity lists built from Data files:
	public static Actor[] Backgrounds; 
	public static Cell[] TerrainTypes;
	public static Actor[] CreatureTypes;
	public static Item[] ItemTypes;
	public static MapSpecial[] MapSpecials;
	
	//GUI
	private GameUI gui;
	
	//Input stuff
	private KeyEvent input;
	private boolean shifted;
	
	private JFileChooser chooser;
	
	//Marks the end of a gaming session. Can't just use isDead(), because we can save and quit.
	boolean quitting;
	public static boolean winner;
	
	//Constructors//////////////////////////////////////////////////////////////////////
	
	public Mythos()
	{		
		winner = false;
		
		
		randomizer = new Random();
		
		logger = new MessageBuffer();
		
		//Initialize Display
		gui = new GameUI(TITLE);
		gui.setFocusable(true);
		if(!gui.hasFocus())
			gui.requestFocus();
		
		gui.addKeyListener(this);
		
		new Thread(gui).start();
		
		game = null; //Will be defined later
		
		input = null;
		
		chooser = new JFileChooser(System.getProperty("user.home") + File.separator + "MythosSavedGames");
		
		//Initialize entity type arrays
		TerrainTypes = FileHandler.loadTerrainTypes();
		ItemTypes = FileHandler.loadItems();
		Backgrounds = FileHandler.loadBackgrounds();
		CreatureTypes = FileHandler.loadActors();
		MapSpecials = FileHandler.loadSpecialRooms();
		
		quitting = true;
	}

	//Methods///////////////////////////////////////////////////////////////////////////
	
	public GameWorld getGame()
	{
		return game;
	}
	
	/**
	 * Get the title page
	 * @return title text
	 */
	private String[] getTitleText()
	{
		String[] text = FileHandler.loadTextFromFile(FileHandler.TITLE_PAGE_FILE, false);
		
		//Add in the version number
		for (int i = 0; i < text.length; i++)
			if (text[i].contains("%VERSION%"))
				text[i] = text[i].replace("%VERSION%", VERSION);
		
		return text;
	}
	
	/**
	 * get help text from file
	 * @param context - what help file to show
	 * @return text
	 */
	private String[] getHelpText(int context)
	{
		//TODO: Load different help files in different situations
		String[] text = FileHandler.loadTextFromFile(FileHandler.MAIN_HELP_PAGE_FILE, false);
		
		//Add in the version number
		for (int i = 0; i < text.length; i++)
			if (text[i].contains("%VERSION%"))
				text[i] = text[i].replace("%VERSION%", VERSION);
		
		return text;
	}
	
	/**
	 * Start a game or exit the program
	 * @return game state
	 */
	public void mainMenu()
	{
		int choice = 0;
		
		boolean loop = true;
		while (loop)
		{
			logger.clearBuffer();
			gui.setQuittingTime(false);
			gui.hideStatus();
			gui.showTitle(getTitleText());
			//Wait for player input
			choice = getInputCode(MAIN_MENU);
			switch(choice)
			{
			case NEW_GAME:
				newGame();
				break;
			case LOAD_GAME:
				loadGame();
				break;
			case EXIT_ESCAPE:
			case EXIT_QUIT:
				loop = false;
				break;
			}
			
			//Quit all the way if asked to
			if (gui.isQuittingTime())
				loop = false;
		}
	}
	
	static String getSaveDirectoryPath()
	{
		//Warning: This property may not work consistently in older Windows systems
		//TODO: At least add check for consistency in Windows XP. Maybe just use a C:\mythossavedgames folder there?
		return System.getProperty("user.home") + File.separator + "MythosSavedGames";
	}
	
	private String getSaveFileName()
	{
		return getSaveDirectoryPath() + File.separator + game.getPC().getName(Entity.INDEFINITE) + "." + VERSION + ".save";
	}
	
	/**
	 * removeSave
	 * delete the save file
	 */
	private void removeSave()
	{
		File save = new File(getSaveFileName());
		
		if (save.exists())
		{
			if(!save.delete())
			{
				Mythos.logger.errorLog("Error handling save file!");
			}
		}
	}
	
	/**
	 * Save the current game state
	 */
	public boolean saveGame()
	{
		File saveDirectory = new File(getSaveDirectoryPath());
		
		if (!saveDirectory.exists() && !saveDirectory.mkdir())
		{
			logger.logMessage("Failed to find/create savegame directory! Save aborted!");
			return false;
		}
		
		File save = new File(getSaveFileName());
		
		//Create the file if it doesn't exist
		if (!save.exists())
		{
			try
			{
				save.createNewFile();
			} catch (IOException e)
			{
				logger.logMessage("Failed to create save file! Save aborted!");
				e.printStackTrace();
				return false;
			}
		}
		
		//Open the file for writing
		ObjectOutputStream out;
		
		try
		{
			out = new ObjectOutputStream(new FileOutputStream(save));
			out.writeObject(game);
			out.close();
		} catch (IOException e)
		{
			logger.logMessage("Failure saving game! Save aborted!");
			e.printStackTrace();
			return false;
		}
		
		//If we get here, everything worked!
		return true;
	}
	
	/**
	 * load a saved game stat
	 */
	public void loadGame()
	{
		int returnVal = chooser.showOpenDialog(null);
		if (returnVal == JFileChooser.APPROVE_OPTION)
		{
			File file = chooser.getSelectedFile();
			
			try
			{
				FileInputStream fin = new FileInputStream(file);
				
				ObjectInputStream oin = new ObjectInputStream(fin);
				
				game = (GameWorld) oin.readObject();
				
				fin.close();
				oin.close();
				
				//Remove the old save file
				removeSave();
				
				quitting = false;
				
				if (game.getPC().hasProperty("WINNER"))
					winner = true;
				else
					winner = false;
				
				startGame();
				
			} catch (IOException | ClassNotFoundException e)
			{
				logger.logMessage("Error: Unable to load save game!");
				e.printStackTrace();
			}
		}
	}
	
	/**
	 * quit the current game
	 */
	public void quitGame()
	{
		//If we have a game in progress
		if (game != null)
			postmortem();
		else
			saveGame();
	}
	
	/**
	 * showInventory
	 * @return whether the player used their turn
	 */
	private boolean showInventory()
	{
		Item[] inventory = game.getPC().listInventory();
		
		int response = MENU_CANCELLED;
		
		boolean tookTime = false;
		
		do
		{
			response = showMenu(INVENTORY_HEADER, getMenuChoices(inventory), LETTERED_MENU, buildChoiceDetails(inventory), "Press 'u' to use or 'd' to drop", new int[]{ KeyEvent.VK_U, KeyEvent.VK_D});
			
			int action = response / MENU_DETAIL_MULT;
			int which = response % MENU_DETAIL_MULT;
			
			//What did the player decide to do?
			switch (action)
			{
			case INV_DROP:
				
				boolean dropAll = true;
				//Is this a stack of items?
				if (game.getPC().seeItem(which).getAmount() > 1)
				{
					int amount = game.getPC().seeItem(which).getAmount(); 
					
					amount = Integer.parseInt(getText("Drop how many(" + amount + ")", String.valueOf(amount), true));
					
					//Zero is a bad amount
					if (amount < game.getPC().seeItem(which).getAmount())
					{
						dropAll = false;
					}

					//Drop a few
					if (amount > 0)
					{
						Item drop = new Item(game.getPC().seeItem(which));
						drop.setAmount(amount);
						
						if (game.getPC().getLocation().getMap().placeItem(drop, game.getPC().getLocation().getX(), game.getPC().getLocation().getX(), true))
						{
							tookTime = true;
							//Reduce the amount we still have
							game.getPC().seeItem(which).setAmount(game.getPC().seeItem(which).getAmount() - amount);
							
							//Sanity check, should never happen
							if (game.getPC().seeItem(which).getAmount() < 1)
								game.getPC().takeItem(which, true);
						}
						else
							Mythos.logger.logMessage("There is no room to drop " + game.getPC().seeItem(which).getName(Entity.DEFINITE));
					}
				}
				
				//If we dropped some don't go any further
				if (dropAll)
				{
					if (game.getPC().dropItem(which))
					{
						tookTime = true;
						response = MENU_CANCELLED;
					}
					else //Tell the player no
						Mythos.logger.logMessage("There is no room to drop " + game.getPC().seeItem(which).getName(Entity.DEFINITE));
				}
				break;
				
			case INV_USE:
				Item item = game.getPC().seeItem(which);
				
				if (item.getUse() == null)
				{
					Mythos.logger.logMessage("That item cannot be used");
					break;
				}
				
				//Use the item
				Cell target = null;
				if (game.getCurrentMap().getTarget() != null)
					target = game.getCurrentMap().getTarget().getLocation();
				tookTime = game.getPC().useItem(which, target);
				
				response = MENU_CANCELLED;
				
				break;
			}
		} while (response != MENU_CANCELLED);

		//Hide menu
		gui.hideTitle();
		gui.showMap(game.getCurrentMap());
		
		return tookTime;
	}
	
	/**
	 * generate a new player & game from scratch
	 */
	public void newGame()
	{
		winner = false;
		quitting = false;
		gui.hideTitle();
		
		//Create the player
		int background;
		String name;
		
		if (DEBUG_AUTO_CHARGEN)
		{
			background = 0;
			name = "Debugger";
		}
		else
		{
			name = getText("Name your character", "Player", false);
			background = showMenu(BIRTH_MENU_HEADER, getMenuChoices(Backgrounds), 
					LETTERED_MENU, buildChoiceDetails(Backgrounds), "Are you sure? (y/N)", SIMPLE_CONFIRM);
		}
		//If user closed the window during birth, just quit
		if (gui.isQuittingTime())
			System.exit(0);
		
		Player pc = new Player(name, Backgrounds[background]);
		
		game = new GameWorld(pc);
		game.changeMap(0, 0);
		
		startGame();
	}
	
	private void startGame()
	{
		gui.showStatus();
		gui.showMap(game.getCurrentMap());
		gui.repaint();
		
		try
		{
			playGame();
		}
		catch(Exception ex)
		{
			ex.printStackTrace();
			if (gui.isQuittingTime())
				quitting = true;
		}
	}
	
	private void takeTurn(Actor actor)
	{
		actor.setDoubleMove();
		actor.takeTurn();
		
		//If actor can still act
		if (actor.canDoubleMove())
			actor.takeTurn();
	}
	
	/**
	 * Mythos game loop
	 */
	public void playGame() throws Exception
	{
		gui.updatePCStatus(game.getPC());
		game.getCurrentMap().updateFOV(game.getPC().getLocation());
		
		Mythos.logger.setGui(gui);
		Mythos.logger.logTransientMessage("Welcome to Mythos!");
		
		Iterator<Actor> actors = null;
		while(!quitting)
		{
			//Process the player and allies
			 actors = game.getCurrentMap().getPcs().iterator();
			
			while (!quitting && actors.hasNext())
			{
				Actor pc = actors.next();
				
				if (pc instanceof Player)
				{
					//loop until the player uses their turn
					while(playerCommand(getInputCode(ANY_KEY)));
					
					//Quit if player is dead
					if (pc.isDead())
						quitting = true;
					else
					{
						game.getCurrentMap().updateFOV(game.getPC().getLocation());
						game.getCurrentMap().updateDMap(GameMap.PC_PATH);
					}
					
					processProjections();
				}
				else
				{
					takeTurn(pc);
					processProjections();
				}
				
				gui.updateTarget();
				gui.updatePCStatus(game.getPC());
				gui.repaint();
			}
			
			//Place following creatures if we can
			game.getCurrentMap().placeFollowers();
			gui.repaint();
			
			//Stop if player closed window or died
			if (gui.isQuittingTime() ||game.getPC().isDead())
				break;
			
			game.getCurrentMap().updateDMap(GameMap.MOB_PATH);
			game.getCurrentMap().updateDMap(GameMap.SMART_MOB_PATH);
			
			//Now process the mobs
			actors = game.getCurrentMap().getMobs().iterator();
			
			while(!quitting && actors.hasNext())
			{
				takeTurn(actors.next());
				processProjections();
				
				//Quit if the player died
				if (game.getPC().isDead())
					quitting = true;
				
				gui.updatePCStatus(game.getPC());
				gui.updateTarget();
				gui.repaint();
			}
			
			//Update mob DMap for allies
			game.getCurrentMap().updateDMap(GameMap.ALLY_PATH);
			game.getCurrentMap().updateDMap(GameMap.SMART_ALLY_PATH);
		}
		
		//Let player see the screen before quitting.
		if (game.getPC().isDead())
		{
			postmortem();
		}
	}
	
	/**
	 * postmortem: show the results of our game, finished or not
	 */
	public void postmortem()
	{
		if (winner)
			logger.logMessage("You are a WINNER! Press any key to return to the menu");
		else
		{
			logger.logMessage("Game over. Press any key to return to the menu");
		}
		getInputCode(ANY_KEY);
	}
	
	/*
	 * processProjections
	 * Run any waiting projections
	 */
	private void processProjections()
	{
		while(game.getCurrentMap().projectionsToRun())
		{
			Projection p = game.getCurrentMap().getNextProjection();
			
			while (p.canProject())
			{
				p.project();
				
				//Animate if projection is visible
				if (p.isSeen())
				{
					gui.updatePCStatus(game.getPC());
					gui.updateTarget();
					gui.repaint();
					
					//Pause so we can see the projection
					try
					{
						Thread.sleep(ANIMATION_DELAY);
					} catch (InterruptedException e) {}
				}
			}
			
			//Cleanup projection animation
			p.cleanup();
		}
			
	}
	
	/**
	 * Player decides to end game
	 * @return whether the player went through with it
	 */
	private boolean suicide()
	{
		if (winner) //Player retires, not commits suicide
		{
			quitting = true;
			return false;
		}
		else
			//Get confirmation
			if (confirmCommand("Give up and end your game permanently? (Use 'S' to save and quit instead)", false))
				if (confirmCommand("Are you sure?", false)) //Are you really sure?
				{
					quitting = true;
					return false;
				}
		
		return true;
	}
	
	/**
	 * display the help text
	 */
	private void showHelp()
	{
		//Show the help file until the player presses a key
		gui.showTitle(getHelpText(HELP_PLAYER_TURN));
		getInputCode(ANY_KEY);
		gui.hideTitle();
		gui.showStatus();
		gui.showMap(game.getCurrentMap());
		gui.repaint();
	}
	
	/**
	 * display message history
	 */
	private void showMessages()
	{
		//Show the help file until the player presses a key
		gui.showTitle(logger.getHistory());
		getInputCode(ANY_KEY);
		gui.hideTitle();
		gui.showStatus();
		gui.showMap(game.getCurrentMap());
		gui.repaint();
	}
	
	/**
	 * getMenuChoices
	 * Helper for showMenu()
	 * Build a list of menu choices from an array of entities
	 * @param entities
	 * @return
	 */
	private String[] getMenuChoices(Entity[] entities)
	{
		String[] choices = new String[entities.length];
		
		for (int i = 0; i < choices.length; i++)
		{
			choices[i] = entities[i].getName(Entity.INDEFINITE);
		}
		
		return choices;
	}
	
	private String[][] buildChoiceDetails(Entity[] entities)
	{
		String[][] details = new String[entities.length][];
		
		for (int i = 0; i < details.length; i++)
		{
			details[i] = entities[i].getDetails();
		}
		
		return details;
	}
	
	/**
	 * Helper function for showMenu()
	 * @param menuHeader
	 * @param menuChoices
	 * @param menuStyle
	 * @param details
	 * @return
	 */
	private String[] buildMenuText(String menuHeader, String[] menuChoices, boolean menuStyle, int choice, String[] details, String confirmInstructions)
	{
		//Indices used below
		int startChoices = 2;
		int endChoices = startChoices + menuChoices.length;
		int startDetails = endChoices + 1;
		int endDetails = startDetails;
		if (details != null)
			endDetails += details.length;
		
		//Determine size of string array
		int arraySize = endDetails + 2;
		
		//Prepare the text to show
		String[] show = new String[arraySize];
		show[0] = menuHeader;
		show[1] = MENU_HEADER_SEPARATOR;
		
		//Sanity check: don't have numbered list if > 10 items
		if (menuChoices.length > 10)
			menuStyle = LETTERED_MENU;
		
		//Add in our choices
		for (int i = 0; i < menuChoices.length; i++)
		{
			if (menuStyle == LETTERED_MENU)
				show[startChoices + i] = "(" + Character.valueOf((char)(KeyEvent.VK_A + i)) + ") " + menuChoices[i];
			else
				show[startChoices + i] = "(" + i + ") " + menuChoices[i];
		}
		
		//And finish with some explanatory text
		show[endChoices] = "";
		show[endDetails] = "";
		
		//If we are showing details, show them
		if (details != null)
		{
			show[endChoices+1] = " " + menuChoices[choice];
			
			for (int j = 0; j < details.length; j++)
				show[startDetails + j] = details[j];
			
			
			show[endDetails+1] = confirmInstructions;	
		}
		else if (menuStyle == NUMBERED_MENU)
			show[endDetails+1] = MENU_INSTRUCTIONS_NUM;
		else
			show[endDetails+1] = MENU_INSTRUCTIONS;
		
		return show;
	}
	
	/**
	 * showMenu
	 * Display a menu to the user and let them pick a choice
	 * @param menuHeader: Text displayed at top of menu
	 * @param menuChoices: list of lines displaying the menu options. 
	 * @param menuStyle: preface the choices with letters or numbers.
	 * @param choiceDetails: String[] of text to be shown when an item is selected, in addition to a confirmation prompt. If null, no confirmation
	 * is offered.
	 * @return [users menu choice][users menu command], or MENU_CANCELLED if the user cancels out.
	 */
	private int showMenu(String menuHeader, String[] menuChoices, boolean menuStyle, String[][] choiceDetails, String confirmInstructions, int[] detailCommands)
	{
		int choice = MENU_CANCELLED;
		
		//Show the initial choices
		gui.showTitle(buildMenuText(menuHeader, menuChoices, menuStyle, MENU_CANCELLED, null, null));
		
		boolean needsConfirm = true;
		if (choiceDetails == null)
			needsConfirm = false;
		
		//Loop until we get a valid choice or the player quits
		while (true)
		{
			if (gui.isQuittingTime())
				return MENU_CANCELLED;
			
			choice = getInputCode(MENU);
			boolean valid = false;

			//Validate input
			if (choice == CANCEL ||
					(menuStyle == NUMBERED_MENU && choice >= 0 && choice <= 9) ||
					(menuStyle == LETTERED_MENU && choice >= KeyEvent.VK_A && choice <= KeyEvent.VK_Z))
				valid = true;
			
			//Check list length
			if (valid)
			{
				//Convert to number 0-26
				if (menuStyle == LETTERED_MENU && choice != CANCEL)
					choice -= KeyEvent.VK_A;
				
				//Now make sure choice is not out-of-bounds
				if (choice != CANCEL && choice >= menuChoices.length)
					valid = false;
			}
			
			if (choice == CANCEL || gui.isQuittingTime())
				return MENU_CANCELLED;
			
			//We have a valid choice
			if (valid)
			{
				//Do we need to confirm the choice
				if(needsConfirm)
				{
					//Get confirmation
					gui.showTitle(buildMenuText(menuHeader, menuChoices, menuStyle, choice, choiceDetails[choice], confirmInstructions));
					
					int choice2 = MENU_CANCELLED;
					while(choice2 < 0)
					{
						choice2 = getInputCode(ANY_KEY);
						
						//If cancel, go back to the menu
						if (choice2 == CANCEL)
							break;
						
						//Check the choice against our list of allowed command values
						boolean pass = false;
						for (int i = 0; i < detailCommands.length; i++)
							if (choice2 == detailCommands[i])
							{
								pass = true;
								break;
							}
						
						if (pass)
							break;
						
						//Warn the player of invalid command
						logger.logTransientMessage("Invalid command. Enter a letter in parentheses or Escape to back up");
					}
					
					//Now if we cancelled, go back and try again
					if (choice2 == CANCEL || (detailCommands == SIMPLE_CONFIRM && choice2 == KeyEvent.VK_N))
					{
						gui.showTitle(buildMenuText(menuHeader, menuChoices, menuStyle, MENU_CANCELLED, null, null));
						continue;
					}
					else
					{
						choice += MENU_DETAIL_MULT * choice2;  //Add choice 2 to choice 1
						break;
					}
				}
				else //we are done
					break;
			}
			
			//If we get here the user entered a bad command
			logger.logTransientMessage("Invalid command. Choose a menu item or Escape to cancel");
		}
		
		//Clear confirmation for simple conf menus
		if (detailCommands == SIMPLE_CONFIRM)
			choice = choice % MENU_DETAIL_MULT;
		  
		return choice;
	}
	
	/**
	 * change to another map
	 * @param mapID
	 * @param exitID
	 */
	private boolean changeMaps()
	{
		if (game.getPC().getLocation() instanceof Exit)
		{
			Exit ex = ((Exit)game.getPC().getLocation());
			int tMap = ex.getTargetMap();
			int tExit = ex.getTargetExit();
			
			game.changeMap(tMap, tExit);
			
			//Update Dmaps
			for (int dm = 0; dm < GameMap.DMAP_ARRAY_SIZE; dm++)
				game.getCurrentMap().updateDMap(dm);
			
			gui.showMap(game.getCurrentMap());
			return true;
		}
		else
		{
			logger.logMessage(game.getPC().getName() + " sees no exit here");
			return false;
		}
	}
	
	private boolean operateTerrain(int dx, int dy)
	{
		Cell targetCell; 
		
		//If dx=dy=0, we are wanting "smart" behavior - find a direction for us, or ask if we're not sure
		if (dx == 0 && dy == 0)
		{
			//Get the adjacent cells to choose from
			LinkedList<Cell> aoc = new LinkedList<Cell>();
			
			for (int x = -1; x < 2; x++)
			{
				for (int y = -1; y < 2; y++)
				{
					//Skip start cell
					if (x == 0 && y == 0)
						continue;
					
					Cell c = game.getCurrentMap().getRelativeLocation(game.getPC().getLocation(), x, y);
					if (c.isOperable())
						aoc.add(c);
				}
			}

			//If there are no operable cells, abort
			if (aoc.size() == 0)
			{
				logger.logTransientMessage("You see nothing to operate here");
			}
			else if (aoc.size() == 1) //If there is only one, then choose it
			{
				dx = aoc.getFirst().getX() - game.getPC().getLocation().getX();
				dy = aoc.getFirst().getY() - game.getPC().getLocation().getY();
			}
			else //Ask the player for a direction
			{
				int direction = getDirection(null);
				
				dx = (direction/10) - 1;
				dy = (direction%10) - 1;
			}
		}
		
		//Now Operate the idicated cell
		dx += game.getPC().getLocation().getX();
		dy += game.getPC().getLocation().getY();
		targetCell = game.getCurrentMap().getLocation(dx, dy);
		
		if (targetCell == null)
		{
			logger.errorLog("operateTerrain: Received null from game.getCurrentMap().getLocation(" + dx + "," + dy +")");
			return false;
		}
		
		if (targetCell.getOperation() == Cell.NO_OP)
		{
			Mythos.logger.logTransientMessage("You see nothing to operate there");
			return false;
		}
		else return game.getPC().operateTerrain(targetCell);

		//return targetCell.operate(1);
	}
	
	private boolean reloadCommand()
	{
		//Must have a gun to shoot
		if (game.getPC().getEquipment(Player.MAINDHAND_SLOT) == null || game.getPC().getEquipment(Player.MAINDHAND_SLOT).getType() != Item.I_GUN)
		{
			Mythos.logger.logTransientMessage("You have nothing to shoot with!");
			return false;
		}
		
		Item weapon = game.getPC().getEquipment(Player.MAINDHAND_SLOT);

		//Make sure item has ammo property
		if (!weapon.hasProperty("ammo"))
		{
			Mythos.logger.errorLog("Item " + weapon.getName(Entity.DEFINITE) + " is of type gun but has no ammo property!");
			return false;
		}
		
		//Get example ammo item
		Item check = Mythos.ItemTypes[weapon.getProperty("ammo")];
		
		//Get inventory
		Item[] inventory = game.getPC().listInventory();
		
		//Cycle through the inventory to find the appropriate ammo
		int match = -1;
		for (int i = 0; i < inventory.length; i++)
		{
			if (inventory[i].compareTo(check) == 0)
			{
				match = i;
				break;
			}
		}
		
		//Is there any ammo?
		if (match < 0)
		{
			Mythos.logger.logTransientMessage("No ammo for " + weapon.getName(Entity.DEFINITE) + "!");
			return false;
		}
		else //load the ammo
		{
			int amount = weapon.getProperty("capacity") - weapon.getProperty("loaded");
			if (amount > inventory[match].getAmount())
				amount = inventory[match].getAmount();
			
			if (amount < 1)
			{
				Mythos.logger.logTransientMessage("No ammo for " + weapon.getName(Entity.DEFINITE) + "!");
				return false;
			}
			
			//Take from backpack
			if (amount == game.getPC().seeItem(match).getAmount())
				game.getPC().takeItem(match, true);
			else
				game.getPC().seeItem(match).setAmount(game.getPC().seeItem(match).getAmount() - amount);
			
			String ammoName = check.getPluralName();
			if (amount == 1)
				ammoName = check.getSingularName();
			
			weapon.setProperty("loaded", weapon.getProperty("loaded") + amount);
			
			logger.logMessage(game.getPC().getName(Entity.DEFINITE) + " loads " + amount + " " + ammoName + " into " + weapon.getName(Entity.DEFINITE));
			
			return true;
		}
	}
	
	private Cell getTarget()
	{
		//TODO: If we don't have a target, use a look-targeting command
		if (game.getCurrentMap().getTarget() == null)
		{
			lookAround();
			
			if (game.getCurrentMap().getTarget() == null)
				return null;
		}

		return game.getCurrentMap().getTarget().getLocation();
	}
	
	/**
	 * THrow an item
	 * @return whether the player took his turn (true) or cancelled
	 */
	private boolean throwCommand()
	{
		Item[] inventory = game.getPC().listInventory();
		String[] choices = new String[inventory.length];
		for (int i = 0; i < choices.length; i++)
			choices[i] = inventory[i].getThrowName();
		
		//THROW_HEADER
		int choice = showMenu(THROW_HEADER, choices, LETTERED_MENU, null, null, null);
		
		//Hide menu
		gui.hideTitle();
		gui.showMap(game.getCurrentMap());
				
		//Cancel if asked
		if (choice == MENU_CANCELLED)
			return false;
		
		//Now pick a target if we don't have one
		Cell target = getTarget();
		
		//User cancelled
		if (target == null)
		{
			logger.logTransientMessage("Cancelled");
			return false;
		}
		
		//Throw at our target
		return game.getPC().throwItem(choice, target);
	}
	
	/**
	 * shoot
	 * Player shoots their weapon
	 * @return
	 */
	private boolean shootCommand()
	{
		//Must have a gun to shoot
		if (game.getPC().getEquipment(Player.MAINDHAND_SLOT) == null || game.getPC().getEquipment(Player.MAINDHAND_SLOT).getType() != Item.I_GUN)
		{
			Mythos.logger.logTransientMessage("You have nothing to shoot with!");
			return false;
		}
		
		Item weapon = game.getPC().getEquipment(Player.MAINDHAND_SLOT);

		//Make sure item has ammo property
		if (!weapon.hasProperty("ammo"))
		{
			Mythos.logger.errorLog("Item " + weapon.getName(Entity.DEFINITE) + " is of type gun but has no ammo property!");
			return false;
		}
		
		//Gun must have ammo to shoot
		if (!weapon.hasProperty("loaded"))
		{
			Mythos.logger.logTransientMessage("You are out of ammo!");
			return false;
		}
		
		//User cancelled
		Cell target = getTarget();
		if (target == null)
		{
			logger.logTransientMessage("Cancelled");
			return false;
		}
		
		//Create shot
		//TODO: Handle higher rates of fire
		Item shot = new Item(ItemTypes[weapon.getProperty("ammo")]);
		
		shot.setAmount(1);
		weapon.setProperty("loaded", weapon.getProperty("loaded") - shot.getAmount());
		
		shot.setProperty("firepower", weapon.getProperty("firepower") + game.getPC().getProperty("firepower"));
		
		return game.getPC().shoot(shot, target);
	}
	
	/**
	 * main: Run the game
	 */
	public static void main(String[] args)
	{
		Mythos yarg = new Mythos();
		yarg.mainMenu();
		System.exit(0);
	}

	//Input handlers///////////////////////////////////////////////////////////
	
	/**
	 * Player command: turn a keypress into an action object for the player
	 * @param input
	 * @return
	 */

	private boolean playerCommand(int command)
	{
		boolean stillPlayerTurn = true;
		
		//Movement commands
		int dx = 0;
		int dy = 0;
		boolean move = false;
				
		switch(command)
		{
		//Movement / Melee / Operate commands
		case MOVE_NW:
		case MOVE_NW2:
			dx--;
		case MOVE_N:
		case MOVE_N2:
		case MOVE_N3:
			dy--;
			move = true;
			break;
			
		case MOVE_SE:
		case MOVE_SE2:
			dx++;
		case MOVE_S:
		case MOVE_S2:
		case MOVE_S3:
			dy++;
			move = true;
			break;
			
		case MOVE_NE:
		case MOVE_NE2:
			dy--;
		case MOVE_E:
		case MOVE_E2:
		case MOVE_E3:
			dx++;
			move = true;
			break;
			
		case MOVE_SW:
		case MOVE_SW2:
			dy++;
		case MOVE_W:
		case MOVE_W2:
		case MOVE_W3:
			dx--;
			move = true;
			break;
			
		case WAIT:
		case WAIT2:
			move = true;
			break;
			
		//Shoot our weapon
		case FIRE:
			stillPlayerTurn = !shootCommand();
			break;
			
		case RELOAD:
			stillPlayerTurn = !reloadCommand();
			break;

		//Throw an item
		case THROW:
			stillPlayerTurn = !throwCommand();
			break;
			
		//Open inventory
		case INVENTORY:
			stillPlayerTurn = !showInventory();
			break;
			
		//List equipment
		case EQUIPMENT:
			stillPlayerTurn = !showEquipment();
			break;
			
		//Operate terrain (open/close doors. Set off traps in your square, etc)
		case OPERATE:
			if (operateTerrain(0,0))
			{
				stillPlayerTurn = false;
			}
			break;
			
		//Interact with ground: pick up item, change level, etc
		case GROUND_ACT:
			//Take stairs
			if (game.getPC().getLocation() instanceof Exit)
				changeMaps();
			//Pick up item
			else 
			{
				stillPlayerTurn = !game.getPC().pickupItem();
				
				if (stillPlayerTurn)
				{
					Mythos.logger.logTransientMessage(game.getPC().getName() + " sees nothing to pick up!");
				}
			}
			break;
			
		//look around
		case LOOK:
			lookAround();
			stillPlayerTurn = true;
			break;
			
		//Show help text
		case SHOW_HELP2:
			if (!shifted)
				break;
		case SHOW_HELP:
			showHelp();
			break;
		
		//Previous messages
		case SHOW_OLD_MESSAGES:
			showMessages();
			
		//Select target
		case NEXT_TARGET:
			game.getCurrentMap().nextTarget();
			gui.updateTarget();
			gui.hideStatus();
			gui.showStatus();
			//gui.updatePCStatus(game.getPC());
			//gui.repaint();
			break;
			
		//Save game and quit
		case SAVE_AND_QUIT:
			if (!shifted)
			{
				game.getPC().swapWeapons();
				gui.updatePCStatus(game.getPC());
				break;
			}
		case EXIT_ESCAPE:
			//If player clicks close, save and quit
			if (gui.isQuittingTime() || confirmCommand("Save game and quit", false))
			{
				//Don't quit if the save fails for some reason
				if (saveGame())
				{
					quitting = true;
					stillPlayerTurn = false;
				}
			}
			break;
		
		//Give up and quit without saving.
		case QUIT_SUICIDE:
			if (!shifted)
				break;
				stillPlayerTurn = suicide();
			break;
				
		default:
			logger.logTransientMessage("Unknown command");
			stillPlayerTurn = true;
		}
		
		//Are we moving/meleeing/operating
		if (move)
		{
			//Don't use a turn if player tries to walk into a wall (unlike a monster, though monsters should never try that in the first place)
			stillPlayerTurn = !game.getPC().move(dx, dy);
		}
		
		return stillPlayerTurn;
	}
	
	private boolean showEquipment()
	{
		int slot = MENU_CANCELLED;
		
		boolean tookTime = false;
		
		//showMenu(String menuHeader, String[] menuChoices, boolean menuStyle, String[][] choiceDetails, String confirmInstructions, int[] detailCommands)
		slot = showMenu(EQUIPMENT_HEADER, game.getPC().getEquipmentMenu(), LETTERED_MENU, null, null, null);
		
		if (slot != MENU_CANCELLED)
		{
			//Did the player want to un-equip something or equip something?
			if (game.getPC().getEquipment(slot) == null)
			{
				int index = showMenu(PICK_EQUIP_HEADER, getMenuChoices(game.getPC().listInventory()), LETTERED_MENU, null, null, null);
				
				//ON cancelling, return to equipment list
				if (slot == MENU_CANCELLED)
					return showEquipment();
				else //equip the item
				{
					Item i = game.getPC().seeItem(index);
					
					if (game.getPC().canEquip(i, slot))
					{
						
						i = game.getPC().takeItem(index, false);
						
						tookTime = game.getPC().equipItem(i, slot);
						
						if (tookTime)
						{
							Mythos.logger.logMessage(game.getPC().getName(Entity.INDEFINITE) + " " + Player.EQUIP_VERB[slot] + " " + i.getName(Entity.DEFINITE));
						}
						else
							Mythos.logger.logMessage("That item cannot be worn there");
					}
					else
						Mythos.logger.logMessage("That item cannot be worn there");
				}
			}
			else //Remove selected equipment
			{
				Item removed = game.getPC().removeEquipment(slot);
				game.getPC().stowItem(removed); //put back in inventory
				Mythos.logger.logMessage(game.getPC().getName(Entity.INDEFINITE) + " " + Player.UNEQUIP_VERB[slot] + " " + removed.getName(Entity.DEFINITE));
				tookTime = true;
			}
		}
		
		//Hide menu
		gui.hideTitle();
		gui.showMap(game.getCurrentMap());
		
		return tookTime;
	}
	
	
	
	/**
	 * lookAround
	 * Look command
	 */
	private void lookAround()
	{
		Mythos.logger.logMessage(LOOK_INSTRUCTIONS);
		
		int keypress = EXIT_ESCAPE;
		game.getCurrentMap().setHighlight(game.getPC().getLocation().getX(), game.getPC().getLocation().getY());
		
		Mythos.logger.logMessage(game.getCurrentMap().getHighlightedCellName());
		
		do
		{
			keypress = getInputCode(ANY_KEY);
			boolean describe = true;
			
			if (keypress != EXIT_ESCAPE)
			{
				int dx = 0;
				int dy = 0;
				//Highlight chosen cell
				switch(keypress)
				{
				case MOVE_NW:
				case MOVE_NW2:
					dx--;
				case MOVE_N:
				case MOVE_N2:
				case MOVE_N3:
					dy--;
					break;
					
				case MOVE_SE:
				case MOVE_SE2:
					dx++;
				case MOVE_S:
				case MOVE_S2:
				case MOVE_S3:
					dy++;
					break;
					
				case MOVE_NE:
				case MOVE_NE2:
					dy--;
				case MOVE_E:
				case MOVE_E2:
				case MOVE_E3:
					dx++;
					break;
					
				case MOVE_SW:
				case MOVE_SW2:
					dy++;
				case MOVE_W:
				case MOVE_W2:
				case MOVE_W3:
					dx--;
					break;
					
				case WAIT:
				case WAIT2:
					break;
					
				case SET_TARGET:
					Cell targetCell = game.getCurrentMap().getLocation(game.getCurrentMap().getHighlightX(), 
							game.getCurrentMap().getHighlightY());
					
					if (targetCell.isOccupied())
					{
						Actor t = targetCell.getOccupant();
						
						boolean found = false;
						for (Actor m: game.getCurrentMap().getMobs())
							if (m == t) //actor is a mob, not an ally
							{
								found = true;
								game.getCurrentMap().setTarget(t);
								gui.updateTarget();
								gui.hideStatus();
								gui.showStatus();
								keypress = EXIT_ESCAPE; //quit the look interface
								gui.overwriteLastMessage("Target set");
								describe = false;
								break;
							}
						
						if (!found)
						{
							gui.overwriteLastMessage("You cannot target an ally!");
							describe = false;
						}
					}
					else
					{
						gui.overwriteLastMessage("There is nothing to target there");
						describe = false;
					}
					break;
				}
				
				game.getCurrentMap().changeHighlightX(dx);
				game.getCurrentMap().changeHighlightY(dy);
				
				//Describe what we see
				if (describe)
					gui.overwriteLastMessage(game.getCurrentMap().getHighlightedCellName());
			}
		} while(keypress != EXIT_ESCAPE);
		
		//Remove the highlighting
		game.getCurrentMap().removeHighlight();
		gui.repaint();
	}

	/**
	 * getText: Interactive text prompt
	 * @param prompt: Instructions for player
	 * @param defaultText: if no text is entered when player hits "Enter", use this
	 * @param numbersOnly: only allow numbers to be typed.
	 * @return
	 */
	private String getText(String prompt, String defaultText, boolean numbersOnly)
	{
		prompt += ": ";
		gui.showTransientMessage(prompt + defaultText);
		StringBuilder text = new StringBuilder();
		
		while(true)
		{
			int code = getInputCode(TEXT_ENTRY);
			
			if (code == CONFIRM_DEFAULT)
			{
				//If blank, use the default
				if (text.length() == 0)
					return defaultText;
				else
					return text.toString();
			}
			else if (code == CANCEL)
			{
				return defaultText;
			}
			else if (code == BACKSPACE || code == BACKSPACE2)
			{
				//Do nothing if no text
				if (text.length() == 0)
					continue;
				
				text.deleteCharAt(text.length() - 1);
				gui.showTransientMessage(prompt + text.toString());
			}
			else if (code >= KeyEvent.VK_A && code <= KeyEvent.VK_Z)
			{
				boolean alpha = false;
				switch(code)
				{
					case KeyEvent.VK_0:
						text.append('0');
						break;
					case KeyEvent.VK_1:
						text.append('1');
						break;
					case KeyEvent.VK_2:
						text.append('2');
						break;
					case KeyEvent.VK_3:
						text.append('3');
						break;
					case KeyEvent.VK_4:
						text.append('4');
						break;
					case KeyEvent.VK_5:
						text.append('5');
						break;
					case KeyEvent.VK_6:
						text.append('6');
						break;
					case KeyEvent.VK_7:
						text.append('7');
						break;
					case KeyEvent.VK_8:
						text.append('8');
						break;
					case KeyEvent.VK_9:
						text.append('9');
						break;
					default:
						alpha = true;
				}
				
				if (alpha && !numbersOnly)
				{
					switch(code)
					{
					case KeyEvent.VK_A:
						text.append(shifted ? 'A' : 'a');
						break;
					case KeyEvent.VK_B:
						text.append(shifted ? 'B' : 'b');
						break;
					case KeyEvent.VK_C:
						text.append(shifted ? 'C' : 'c');
						break;
					case KeyEvent.VK_D:
						text.append(shifted ? 'D' : 'd');
						break;
					case KeyEvent.VK_E:
						text.append(shifted ? 'E' : 'e');
						break;
					case KeyEvent.VK_F:
						text.append(shifted ? 'F' : 'f');
						break;
					case KeyEvent.VK_G:
						text.append(shifted ? 'G' : 'g');
						break;
					case KeyEvent.VK_H:
						text.append(shifted ? 'H' : 'h');
						break;
					case KeyEvent.VK_I:
						text.append(shifted ? 'I' : 'i');
						break;
					case KeyEvent.VK_J:
						text.append(shifted ? 'J' : 'j');
						break;
					case KeyEvent.VK_K:
						text.append(shifted ? 'K' : 'k');
						break;
					case KeyEvent.VK_L:
						text.append(shifted ? 'L' : 'l');
						break;
					case KeyEvent.VK_M:
						text.append(shifted ? 'M' : 'm');
						break;
					case KeyEvent.VK_N:
						text.append(shifted ? 'N' : 'n');
						break;
					case KeyEvent.VK_O:
						text.append(shifted ? 'O' : 'o');
						break;
					case KeyEvent.VK_P:
						text.append(shifted ? 'P' : 'p');
						break;
					case KeyEvent.VK_Q:
						text.append(shifted ? 'Q' : 'q');
						break;
					case KeyEvent.VK_R:
						text.append(shifted ? 'R' : 'r');
						break;
					case KeyEvent.VK_S:
						text.append(shifted ? 'S' : 's');
						break;
					case KeyEvent.VK_T:
						text.append(shifted ? 'T' : 't');
						break;
					case KeyEvent.VK_U:
						text.append(shifted ? 'U' : 'u');
						break;
					case KeyEvent.VK_V:
						text.append(shifted ? 'V' : 'v');
						break;
					case KeyEvent.VK_W:
						text.append(shifted ? 'W' : 'w');
						break;
					case KeyEvent.VK_X:
						text.append(shifted ? 'X' : 'x');
						break;
					case KeyEvent.VK_Y:
						text.append(shifted ? 'Y' : 'y');
						break;
					case KeyEvent.VK_Z:
						text.append(shifted ? 'Z' : 'z');
						break;
					}
				}
				gui.showTransientMessage(prompt + text.toString());
			}
			else
			{
				text.append(Character.toChars(code));
				gui.showTransientMessage(prompt + text.toString());
			}
		}
	}
	
	/**
	 * getDirection: get a direction from the player
	 * @param prompt
	 * @return
	 */
	private int getDirection(String prompt)
	{
		//Default prompt
		if (prompt == null)
			prompt = "Which direction?";
		
		gui.showTransientMessage(prompt);
		
		int dx = 1;
		int dy = 1;
		
		switch(getInputCode(DIRECTION))
		{
		case MOVE_NW:
		case MOVE_NW2:
			dx--;
		case MOVE_N:
		case MOVE_N2:
		case MOVE_N3:
			dy--;
			break;
			
		case MOVE_SE:
		case MOVE_SE2:
			dx++;
		case MOVE_S:
		case MOVE_S2:
		case MOVE_S3:
			dy++;
			break;
			
		case MOVE_NE:
		case MOVE_NE2:
			dy--;
		case MOVE_E:
		case MOVE_E2:
		case MOVE_E3:
			dx++;
			break;
			
		case MOVE_SW:
		case MOVE_SW2:
			dy++;
		case MOVE_W:
		case MOVE_W2:
		case MOVE_W3:
			dx--;
			break;

		case CANCEL:
			return -1;
		}
		
		return dx * 10 + dy;
	}
	
	private boolean confirmCommand(String message, boolean confirmByDefault)
	{
		boolean confirmation = confirmByDefault;
		
		if (confirmation)
			message += " (Y/n)?";
		else
			message += "(y/N)?";
		
		gui.showTransientMessage(message);
		
		switch(getInputCode(CONFIRM))
		{
		case CONFIRM_YES:
			confirmation = true;
			break;
		case CONFIRM_NO:
		case CANCEL:
			confirmation = false;
			break;
		default:
			//Leave with default confirmation
		}
		
		gui.showTransientMessage("");
		return confirmation;
	}
	
	private boolean isDirectionCommand(int command)
	{
		//TODO: Add cancel/back
		switch(command)
		{
		case MOVE_N:
		case MOVE_N2:
		case MOVE_N3:
		case MOVE_S:
		case MOVE_S2:
		case MOVE_S3:
		case MOVE_E:
		case MOVE_E2:
		case MOVE_E3:
		case MOVE_W:
		case MOVE_W2:
		case MOVE_W3:
		case MOVE_NW:
		case MOVE_NW2:
		case MOVE_NE:
		case MOVE_NE2:
		case MOVE_SW:
		case MOVE_SW2:
		case MOVE_SE:
		case MOVE_SE2:
		case WAIT:
		case WAIT2:
			return true;
		}
		
		return false;
	}
	
	//Get command
	private int getInputCode(int type)
	{
		int code = -99;
		
		//Wait for input
		while(input == null)
		{
			try
			{
				Thread.sleep(50);
			} catch (InterruptedException e) {}
			
			if (input != null)
				switch(type)
				{
				case CONFIRM:
					if (input.getKeyCode() != CONFIRM_YES && input.getKeyCode() != CONFIRM_NO
							&& input.getKeyCode() != CONFIRM_DEFAULT && input.getKeyCode() != CONFIRM_DEFAULT2)
						input = null;
					break;
					
				case MENU:
					//Accept only escape or alphanumeric
					if (input.getKeyCode() != CANCEL && (input.getKeyCode() < KeyEvent.VK_0 || input.getKeyCode() > KeyEvent.VK_9 ) && 
							(input.getKeyCode() < KeyEvent.VK_A || input.getKeyCode() > KeyEvent.VK_Z))
						input = null;
					break;
					
				case DIRECTION:
					if (!isDirectionCommand(input.getKeyCode()))
						input = null;
					break;
					
				case ANY_KEY:
					break;
				}
			
			//If player closed the window, exit
			if (gui.isQuittingTime())
			{
				//game.getPC().quitting = true;
				return EXIT_ESCAPE;
			}
		}
		
		code = input.getKeyCode();
		input = null;
		
		return code;
	}
	
	@Override
	public void keyPressed(KeyEvent arg0)
	{
		if (arg0.getKeyCode() == KeyEvent.VK_SHIFT)
		{
			if (!shifted)
				shifted = true;
		}
		else
			input = arg0;
	}

	@Override
	public void keyReleased(KeyEvent arg0)
	{
		if (arg0.getKeyCode() == KeyEvent.VK_SHIFT)
			if (shifted)
				shifted = false;
	}

	@Override
	public void keyTyped(KeyEvent arg0)
	{
		;
	}
	
	public static int callRNG(int min, int max)
	{
		if (min < 0)
		{
			min = 0;
		}
		
		if (min > max)
		{
			System.out.println("Invalid callRNG! Min > Max; reducing Min");
			min = max;
		}
		
		if (min == max)
			return max;
		
		//Finally, we can call the RNG
		return min + randomizer.nextInt(1 + max - min);  //3-6 min=3 max = 6. 6-3 = 3. nextInt(3+1) gives 0-3, +3 = 3-6
	}
	
	public static int callRNG(int max)
	{
		return callRNG(0, max);
	}
	
	static Actor randomMob(int level)
	{
		LinkedList<Actor> bestiary = new LinkedList<Actor>();
		
		for (Actor a: CreatureTypes)
		{
			if (a.getLevel() <= level)
				for (int j = 0; j < a.getRarity(); j++)
					bestiary.add(a);
		}
		
		Actor pick = new Actor(bestiary.get(Mythos.callRNG(bestiary.size() - 1)));
		
		return pick;
	}
	
	static Item genItem(int id)
	{
		Item pick = new Item(ItemTypes[id]);
		
		//Some items come in stacks
		if (pick.hasProperty("stack") || pick.hasProperty("levelstack"))
		{
			pick.setAmount(Mythos.callRNG(1, pick.getProperty("stack")));
		}
		
		return pick;
	}
	
	static Item randomItem(int level)
	{
		LinkedList<Item> catalog = new LinkedList<Item>();
		
		for (Item i: ItemTypes)
		{
			if (i.getLevel() <= level)
			{
				for (int j = 0; j < i.getRarity(); j++)
					catalog.add(i);
			}
		}
		
		Item pick = genItem(catalog.get(Mythos.callRNG(catalog.size() - 1)).level);
		
		//Some items come in stacks
		if (pick.hasProperty("levelstack"))
		{
			int dice = level;
			int sides = pick.getProperty("levelstack");
			int stackSize = 0;
			
			while (dice > 0)
			{
				stackSize += Mythos.callRNG(1, sides);
				dice--;
			}
			
			pick.setAmount(stackSize);
		}
		
		return pick;
	}
}
